Android开发高手课笔记 - Chapter03

Android开发高手课 【第三节】 课后作业解析 —— 内存优化

本节主要是讲了内存优化相关的工作,其中比较重要的是内存分配日志的获取,也就是demo中的部分。项目中主要讲了使用一些Hook框架,Hook分配对象时候的 RecordAllocation 方法,在分配对象的时候,计算对象的大小以及分配的对象类名。

Demo首先给分配对象的方法Hook了一个最大值,当超过最大值的时候,就将内存分配dump到本地:

1
tracker.initForArt(BuildConfig.VERSION_CODE, 5000);//从 start 开始触发到5000的数据就 dump 到文件中

上面的函数就是将分配对象的最大值设置为5000,当分配对象的数量大于5000的时候,就会讲内存分配的日志dump到本地。

当分配对象达到5000的时候,LogCat会输出如下语句:

1
2
3
4
09-27 14:05:44.554 12228-12228/com.dodola.alloctrack I/AllocTracker: artGetRecentAllocations finished
09-27 14:05:44.554 12228-12228/com.dodola.alloctrack I/AllocTracker: artGetRecentAllocations success
09-27 14:05:44.556 12228-12228/com.dodola.alloctrack I/AllocTracker: saveARTAllocationData /storage/emulated/0/crashDump/1569564344 file, fd: 28
09-27 14:05:44.558 12228-12228/com.dodola.alloctrack I/AllocTracker: saveARTAllocationData write file to /storage/emulated/0/crashDump/1569564344

表示 /storage/emulated/0/crashDump/1569564344 就是已经dump下来的日志分配文件了。

接下来使用DumpPrinter,将日志文件解析出来(首先要将日志文件 adb pull 出来:

1
java -jar tools/DumpPrinter-1.0.jar 1569564345 > dumpLog.txt

打开之后就可以看到分配对象的内容了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Found 988 records:
tid=1 float[] (224 bytes)
com.android.internal.view.animation.FallbackLUTInterpolator.createLUT (FallbackLUTInterpolator.java:49)
com.android.internal.view.animation.FallbackLUTInterpolator.createNativeInterpolator (FallbackLUTInterpolator.java:67)
android.view.RenderNodeAnimator.applyInterpolator (RenderNodeAnimator.java:169)
android.view.RenderNodeAnimator.start (RenderNodeAnimator.java:185)
android.graphics.drawable.RippleComponent$RenderNodeAnimatorSet.start (RippleComponent.java:309)
android.graphics.drawable.RippleComponent.startPendingAnimation (RippleComponent.java:192)
android.graphics.drawable.RippleComponent.draw (RippleComponent.java:159)
android.graphics.drawable.RippleDrawable.drawBackgroundAndRipples (RippleDrawable.java:893)
android.graphics.drawable.RippleDrawable.draw (RippleDrawable.java:694)
android.view.View.getDrawableRenderNode (View.java:16421)
android.view.View.drawBackground (View.java:16357)
android.view.View.draw (View.java:16169)
android.view.View.updateDisplayListIfDirty (View.java:15174)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3593)
android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:3573)
android.view.View.updateDisplayListIfDirty (View.java:15134)
tid=1 char[] (192 bytes)
java.lang.AbstractStringBuilder.enlargeBuffer (AbstractStringBuilder.java:95)
java.lang.AbstractStringBuilder.append0 (AbstractStringBuilder.java:133)
java.lang.StringBuilder.append (StringBuilder.java:124)
libcore.reflect.InternalNames.getInternalName (InternalNames.java:84)
libcore.reflect.AnnotationAccess.getAnnotation (AnnotationAccess.java:193)
libcore.reflect.AnnotationAccess.isDeclaredAnnotationPresent (AnnotationAccess.java:180)
libcore.reflect.AnnotationAccess.isAnnotationPresent (AnnotationAccess.java:137)
java.lang.Class.isAnnotationPresent (Class.java:1228)
android.view.RenderNodeAnimator.isNativeInterpolator (RenderNodeAnimator.java:158)
android.view.RenderNodeAnimator.applyInterpolator (RenderNodeAnimator.java:165)
android.view.RenderNodeAnimator.start (RenderNodeAnimator.java:185)
android.graphics.drawable.RippleComponent$RenderNodeAnimatorSet.start (RippleComponent.java:309)
android.graphics.drawable.RippleComponent.startPendingAnimation (RippleComponent.java:192)
android.graphics.drawable.RippleComponent.draw (RippleComponent.java:159)
android.graphics.drawable.RippleDrawable.drawBackgroundAndRipples (RippleDrawable.java:893)
android.graphics.drawable.RippleDrawable.draw (RippleDrawable.java:694)
....

这里和使用Android Studio 自带的profile分析的内存是一样的。

接着看一下Hook的native方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void hookFunc() {
LOGI("start hookFunc");
void *handle = ndk_dlopen("libart.so", RTLD_LAZY | RTLD_GLOBAL);

if (!handle) {
LOGE("libart.so open fail");
return;
}
void *hookRecordAllocation26 = ndk_dlsym(handle,
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj");

void *hookRecordAllocation24 = ndk_dlsym(handle,
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj");

void *hookRecordAllocation23 = ndk_dlsym(handle,
"_ZN3art3Dbg16RecordAllocationEPNS_6ThreadEPNS_6mirror5ClassEj");

void *hookRecordAllocation22 = ndk_dlsym(handle,
"_ZN3art3Dbg16RecordAllocationEPNS_6mirror5ClassEj");

//此处说明一下26和24版本需要使用 hookzz 的原因。
//hookzz 框架有个优势是可以获取方法进入时候的寄存器内容,而很多时候我们要根据r0来获取 this
if (hookRecordAllocation26 != nullptr) {
LOGI("Finish get symbol26");
// ZzWrap((void *) hookRecordAllocation26, beforeRecordAllocation, nullptr);
MSHookFunction(hookRecordAllocation26, (void *) &newArtRecordAllocation26,
(void **) &oldArtRecordAllocation26);

} else if (hookRecordAllocation24 != nullptr) {
LOGI("Finish get symbol24");
// ZzWrap((void *) hookRecordAllocation24, beforeRecordAllocation, nullptr);
MSHookFunction(hookRecordAllocation26, (void *) &newArtRecordAllocation26,
(void **) &oldArtRecordAllocation26);

} else if (hookRecordAllocation23 != NULL) {
LOGI("Finish get symbol23");
MSHookFunction(hookRecordAllocation23, (void *) &newArtRecordAllocation23,
(void **) &oldArtRecordAllocation23);
} else {
LOGI("Finish get symbol22");
if (hookRecordAllocation22 == NULL) {
LOGI("error find hookRecordAllocation22");
return;
} else {
MSHookFunction(hookRecordAllocation22, (void *) &newArtRecordAllocation22,
(void **) &oldArtRecordAllocation22);
}
}
dlclose(handle);
}

可以看到针对SDK版本22、23、24、26都有不同的实现,但实际上如果通过函数的地址解析到方法,都是RecordAllocation这个方法,只是在由于在不同的SDK版本上实现不同,所以有了不同的hook。

在Hook了原始的RecordAllocation方法之后,调 MSHookFunction(hookRecordAllocation26, (void *) &newArtRecordAllocation26, (void **) &oldArtRecordAllocation26); 将其替换为 newArtRecordAllocation26,最终实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static bool newArtRecordAllocationDoing24(void *_this, Class *type, size_t byte_count) {

if (artAllocMapClear == nullptr) {//如果无法主动 clear 对象,那么下面的逻辑会导致 dump 下来的对象重复
return false;
}

allocObjectCount++;

char *typeName = GetDescriptor(type, &a);
// LOGI("=====class name:%s,allocbyte:%d", typeName,byte_count);// 如果只关心分配的对象大小的话,可以不用做alloc dump 的操作
//达到 max
int randret = randomInt(0, 100);
if (randret == LUCKY) {
LOGI("====current alloc count %d=====", allocObjectCount.load());
return false;
}
// int artAllocMax = getARTAllocRecordMax();
//Hook方法的实现,当分配的对象大于指定的最大值的时候,就将其日志文件调用saveData方法,dump到本地
if (allocObjectCount > setAllocRecordMax) {
CMyLock lock(g_Lock);
allocObjectCount = 0;

//write alloc data to file
jbyteArray allocData = getARTAllocationData();
SaveAllocationData saveData{allocData};
saveARTAllocationData(saveData);
//TODO:clear alloc data
if (artAllocMapClear != nullptr) {
artAllocMapClear(_this);
LOGI("===========CLEAR ALLOC MAPS=============");
}

lock.Unlock();
}
return true;
}

由这章内容可以看出,如果要自定义内存的捕捉、自动化获取,需要对Art / Dalvik 虚拟机有很深入的了解,对于其一些native方法也要掌握。之后要加强在这方面的学习,再遇到问题的时候才能全面的,彻底的分析,解决。

本节demo在【华为 荣耀 V9 Android P】以及 【模拟器:Nexus 6 Android 7.0】上都无效果。换成了 【模拟器: Nexus 4 Android 6.0】才可以正常输出。